import * as satellite from 'satellite.js'
import type { SatRec, EciVec3, Kilometer, GeodeticLocation } from 'satellite.js'
import dayjs from 'dayjs'

interface Pass {
  name: string
  start: number
  end?: number
  duration?: number
  azimuthStart: number
  maxElevation: number
  azimuthApex: number
  apex?: number
  azimuthEnd?: number
}

const deg2rad = Math.PI / 180

export default class Orbit {
  private name: string
  private tle: string[]
  private satrec: SatRec

  constructor(name: string, tle: string) {
    this.name = name
    this.tle = tle.split('\n')
    this.satrec = satellite.twoline2satrec(this.tle[1], this.tle[2])
  }

  public get satnum(): string {
    return this.satrec.satnum
  }

  public get orbitalPeriod(): number {
    const meanMotionRad = this.satrec.no
    return (2 * Math.PI) / meanMotionRad
  }

  private positionECI(time: Date): EciVec3<Kilometer> | boolean {
    return satellite.propagate(this.satrec, time).position
  }

  positionECF(time: Date) {
    const positionEci = this.positionECI(time)
    const gmst = satellite.gstime(time)

    return satellite.eciToEcf(positionEci as EciVec3<Kilometer>, gmst)
  }

  positionGeodetic(time: Date): GeodeticLocation {
    const positionEci = this.positionECI(time)
    const gmst = satellite.gstime(time)
    const positionGd = satellite.eciToGeodetic(positionEci as EciVec3<Kilometer>, gmst)
    return { ...positionGd, height: positionGd.height * 1000 }
  }

  positionGeodeticWithVelocity(timestamp: Date) {
    const PositionAndVelocity = satellite.propagate(this.satrec, timestamp)
    const positionEci = PositionAndVelocity.position
    const velocityEci = PositionAndVelocity.velocity as satellite.EciVec3<number>
    const gmst = satellite.gstime(timestamp)
    const positionGd = satellite.eciToGeodetic(positionEci as EciVec3<Kilometer>, gmst)
    const velocity = Math.sqrt(velocityEci.x ** 2 + velocityEci.y ** 2 + velocityEci.z ** 2)
    return { ...positionGd, height: positionGd.height * 1000, velocity }
  }

  computePassesElevation(
    groundStationPosition: GeoPosition,
    startDate = dayjs().toDate(),
    endDate = dayjs(startDate).add(7, 'day').toDate(),
    minElevation = 1,
    maxPasses = 50
  ): Pass[] {
    const groundStation = {
      ...groundStationPosition,
      longitude: groundStationPosition.longitude * deg2rad,
      latitude: groundStationPosition.latitude * deg2rad,
      height: groundStationPosition.height / 1000
    }

    const date = new Date(startDate)
    const passes: Pass[] = []
    const pass: Pass = { name: this.name, start: 0, azimuthStart: 0, maxElevation: 0, azimuthApex: 0 }
    let ongoingPass = false
    let lastElevation = 0

    while (date < endDate && passes.length <= maxPasses) {
      const positionEcf = this.positionECF(date)
      const lookAngles = satellite.ecfToLookAngles(groundStation, positionEcf)
      const elevation = lookAngles.elevation / deg2rad
      if (elevation > 0) {
        if (!ongoingPass) {
          // Start of new pass
          pass.start = date.getTime()
          pass.azimuthStart = lookAngles.azimuth
          pass.maxElevation = elevation
          pass.azimuthApex = lookAngles.azimuth
          ongoingPass = true
        } else if (elevation > pass.maxElevation) {
          // Ongoing pass
          pass.maxElevation = elevation
          pass.apex = date.getTime()
          pass.azimuthApex = lookAngles.azimuth
        }
        date.setSeconds(date.getSeconds() + 5)
      } else if (ongoingPass) {
        // End of pass
        if (pass.maxElevation > minElevation) {
          pass.end = date.getTime()
          pass.duration = pass.end - pass.start
          pass.azimuthEnd = lookAngles.azimuth
          passes.push({
            ...pass,
            azimuthStart: pass.azimuthStart / deg2rad,
            azimuthApex: pass.azimuthApex / deg2rad,
            azimuthEnd: pass.azimuthEnd / deg2rad
          })
        }
        ongoingPass = false
        lastElevation = -180
        date.setMinutes(date.getMinutes() + this.orbitalPeriod * 0.75)
      } else {
        const deltaElevation = elevation - lastElevation
        lastElevation = elevation
        if (deltaElevation < 0) {
          date.setMinutes(date.getMinutes() + this.orbitalPeriod * 0.75)
          lastElevation = -180
        } else if (elevation < -20) {
          date.setMinutes(date.getMinutes() + 5)
        } else if (elevation < -5) {
          date.setMinutes(date.getMinutes() + 1)
        } else if (elevation < -1) {
          date.setSeconds(date.getSeconds() + 5)
        } else {
          date.setSeconds(date.getSeconds() + 2)
        }
      }
    }
    return passes
  }
}
